跳到主要内容

手写 Babel: parser 篇

parser 的功能是把源码转成 AST,支持各种语法的 parse。

babel 的 parser 并不是从零自己实现的,而是基于 acron 做了扩展。在 《js parser 的历史》那一节大部分讲过 js parser 都是 estree 标准的,acorn 也是 estree 标准的实现,支持插件,

babel 就是基于 acorn,然后实现了 jsx、typescript、flow 等语法插件的扩展,并且修改了一些 AST,比如 Literal 扩展为了 StringLitreal、NumericLiteral 等。

所以,我们也不会从零实现 parser,也会采用基于 acron 扩下扩展的方式。

思路分析​

acorn 插件的实现方式是继承之前的 Parser 返回新的 Parser,重写一些方法来做 AST 修改和扩充。

比如:

module.exports = function (Parser) {
return class extends Parser {
parseLiteral(...args) {
const node = super.parseLiteral(...args);
switch (typeof node.value) {
case "number":
node.type = "NumericLiteral";
break;
case "string":
node.type = "StringLiteral";
break;
}
return node;
}
};
};

这是我们之前实现过的,把 Literal 扩展为 StringLiteral、NumericLiteral 等的一个插件。

之前还实现过扩展一个 guang 的关键字的插件。

我们希望提供这种 api:

const ast = parser.parse(sourceCode, {
plugins: ["literal", "guangKeyword"],
});

也就是根据传入的 plugins 来确定使用什么插件,然后返回扩展以后的 parser。实现方式就是保存一个插件的 map,按照传入的插件名使用就行。

代码实现​

我们把插件放到不同的模块中,然后通过 map 来维护:

const syntaxPlugins = {
literal: require("./plugins/literal"),
guangKeyword: require("./plugins/guangKeyword"),
};

之后实现 parse 的时候,先把 options 做合并,之后根据 plugin 来依此启用不同的插件。

const defaultOptions = {
plugins: [],
};

function parse(code, options) {
const resolvedOptions = Object.assign({}, defaultOptions, options);

const newParser = resolvedOptions.plugins.reduce((Parser, pluginName) => {
let plugin = syntaxPlugins[pluginName];
return plugin ? Parser.extend(plugin) : Parser;
}, acorn.Parser);

return newParser.parse(code, {
locations: true,
});
}

这里要指定 locations 为 true,也就是保留 AST 在源码中的位置信息,这个在生成 sourcemap 的时候会用的。

这样就实现了 parse 和语法插件功能。

总结​

parser 负责把源码转成 AST,js parser 大多是符合 estree 的标准的,acorn 也是对它的实现。

acorn 支持插件,可以扩展语法,babel parser 就是 fork 了 acorn 做了扩展,我们也通过类似的方式,实现了两个语法插件,然后通过 options 启用。

当然,我们没有实现类似 jsx、typescript 这种复杂语法插件。我们的目的只是理清 babel 实现思路,而不是做一个完善的 babel。

(代码在这里,建议 git clone 下来通过 node 跑一下)